/*
 * Copyright (C) 2010 Fores Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package jp.fores.midori.client.handler;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import jp.fores.midori.client.RPCClient;
import jp.fores.midori.core.dto.RPCInputDto;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * リモートプロシージャーコール用を行うクライアントの通信処理を行うハンドラの最も基本的な実装クラス。<br>
 * Java標準の <code>{@link HttpURLConnection}</code> クラスを使って処理を行います。<br>
 * (このクラスの全てのメソッドはスレッドセーフです。)<br>
 */
public class BasicRPCHandler implements RPCHandler {
	//==========================================================
	//定数

	/**
	 * ログ出力用
	 */
	private static final Log log = LogFactory.getLog(BasicRPCHandler.class);

	/**
	 * バッファサイズ
	 */
	private static final int BUFFER_SIZE = 8192;


	//==========================================================
	//メソッド

	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//抽象メソッドの実装

	/**
	 * {@inheritDoc}
	 */
	@Override
	public Object execute(RPCClient rpcClient, URL url, RPCInputDto inputDto)
			throws Exception {
		//==========================================================
		//サーバーに接続

		//サーバーに接続するためのURLConnectionを取得し、HttpURLConnectionにキャストする
		HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();

		//入出力を両方とも可能にする
		urlConnection.setDoInput(true);
		urlConnection.setDoOutput(true);

		//内部バッファ処理を行わずにHTTP要求本体のストリーミングを有効にする
		//(コンテンツ長には入力データ用のDTOをシリアライズした場合に出力されるサイズを指定)
		//(こうしないとストリームが動的にContent-Lengthを計算しようとするために、ストリームを閉じるまで全てのデータをバッファリングしてしまい、
		// その結果内部バッファがあふれてしまうことがある)
		//(TODO:とりあえず保留)
		//urlConnection.setFixedLengthStreamingMode((int) MidoriSerializationUtils.countSerializeSize(inputDto));

		//リクエストメソッドを設定
		urlConnection.setRequestMethod(rpcClient.requestMethod);

		//接続タイムアウト時間を設定
		urlConnection.setConnectTimeout(rpcClient.connectTimeout);

		//読み取りタイムアウト時間を設定
		urlConnection.setReadTimeout(rpcClient.readTimeout);

		//「User-Agent」ヘッダに自分の単純なクラス名を設定
		urlConnection.addRequestProperty("User-Agent", this.getClass().getSimpleName());

		//「Content-Type」ヘッダを設定
		//(rpcClient本体ではなく、rpcClientの中のserializerから取得する)
		urlConnection.addRequestProperty("Content-Type", rpcClient.serializer.getContentType());

		//リクエスト送信時にgzip圧縮を行う場合
		if (rpcClient.requestGZip) {
			//「Content-Encoding」用のヘッダを設定
			urlConnection.addRequestProperty(rpcClient.getContentEncodingHeaderName(), "gzip");
		}

		//レスポンスでgzip圧縮を受け入れる場合
		if (rpcClient.responseGZip) {
			//「Accept-Encoding」用のヘッダを設定
			urlConnection.addRequestProperty(rpcClient.getAcceptEncodingHeaderName(), "gzip,deflate");
		}

		//サーバーへの通信リンクを確立する
		urlConnection.connect();


		//==========================================================
		//サーバーに入力データを渡す

		//サーバーにデータを送るための出力ストリームを取得
		OutputStream outputStream = urlConnection.getOutputStream();

		try {
			//リクエスト送信時にgzip圧縮を行う場合
			if (rpcClient.requestGZip) {
				//デバッグログが有効な場合
				if (log.isDebugEnabled()) {
					//デバッグログ出力
					log.debug("リクエスト送信時にgzip圧縮を行います。");
				}

				//出力ストリームをGZIPOutputStreamでラッピングする
				outputStream = new GZIPOutputStream(outputStream);
			}
			//それ以外の場合
			else {
				//出力ストリームをBufferedOutputStreamでラッピングする
				outputStream = new BufferedOutputStream(outputStream, BUFFER_SIZE);
			}

			//RPCの入力データを格納するためのDTOクラスの内容を、出力ストリームに出力する
			//(サーバーに入力データを送る)
			//(rpcClientの中のserializerに処理を委譲する)
			rpcClient.serializer.inputDtoToOutputStream(inputDto, outputStream);
		}
		//終了処理
		finally {
			//出力ストリームを確実に閉じる
			//(ここで出力ストリームを閉じなくても、入力ストリームを開こうとすると自動的に閉じられるようだが、念のために閉じておく)
			if (outputStream != null) {
				try {
					outputStream.close();
				}
				//例外処理
				catch (Exception e) {
					//どうしようもないので、例外はもみ消す
				}
			}
		}


		//==========================================================
		//サーバーからの出力データを取得する

		try {
			//サーバーからのデータを取得するための入力ストリームを取得
			InputStream inputStream = urlConnection.getInputStream();

			try {
				//レスポンスの「Content-Encoding」用のヘッダの内容を取得
				String contentEncoding = urlConnection.getHeaderField(rpcClient.getContentEncodingHeaderName());

				//レスポンスの「Content-Encoding」用のヘッダに「gzip」という文字列が含まれている場合
				if ((contentEncoding != null)
						&& (contentEncoding.indexOf("gzip") != -1)) {
					//デバッグログが有効な場合
					if (log.isDebugEnabled()) {
						//デバッグログ出力
						log.debug("レスポンスがgzip圧縮されているので、展開しつつ読み込みます。");
					}

					//入力ストリームをGZIPInputStreamでラッピングする
					inputStream = new GZIPInputStream(inputStream);
				}
				//それ以外の場合
				else {
					//入力ストリームをBufferedInputStreamでラッピングする
					inputStream = new BufferedInputStream(inputStream, BUFFER_SIZE);
				}

				//レスポンスの入力ストリームを処理して、リモートプロシージャコールの戻り値を取得
				//(rpcClientの中のserializerに処理を委譲する)
				Object result = rpcClient.serializer.handleRPCResponseInputStream(inputStream);

				//デバッグログが有効な場合
				if (log.isDebugEnabled()) {
					//デバッグログ出力
					log.debug("*** 「" + url + "」に対するリモートプロシージャコールが正常終了しました ***");
				}

				//リモートプロシージャコールの戻り値を返す
				return result;
			}
			//終了処理
			finally {
				//入力ストリームを確実に閉じる
				if (inputStream != null) {
					try {
						inputStream.close();
					}
					//例外処理
					catch (Exception e) {
						//どうしようもないので、例外はもみ消す
					}
				}
			}
		}
		//例外処理
		catch (Exception e) {
			//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
			//エラーストリームを読み捨てる
			//(エラーの場合は、エラーストリームを最後まで読み込まないと接続が再利用できないらしいため)

			//サーバーからのデータを取得するためのエラーストリームを取得
			InputStream errorStream = urlConnection.getErrorStream();

			//エラーストリームが取得できた場合
			if (errorStream != null) {
				//デバッグログが有効な場合
				if (log.isDebugEnabled()) {
					//デバッグログ出力
					log.debug("エラーストリームを読み捨てます。");
				}

				try {
					//作業用のバイト配列を生成
					byte[] byteArray = new byte[4096];

					//エラーストリームの最後まで読み続ける
					while (errorStream.read(byteArray) != -1) {
						//読み込んだ内容はいらないので、そのまま捨てる
					}
				}
				//終了処理
				finally {
					//エラーストリームを確実に閉じる
					try {
						errorStream.close();
					}
					//例外処理
					catch (Exception ee) {
						//どうしようもないので、例外はもみ消す
					}
				}
			}
			//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/


			//コネクションの切断を促す
			//(これをやっておかないと、CLOSE_WAITの状態が長時間残ってしまう可能性があるため)
			urlConnection.disconnect();

			//捕捉した例外をそのまま投げる
			throw e;
		}

	}
}
